#include "callother.h"
#include "compiler_unit.h"

#define MAX_MOD_NAME_LEN    200

static CoValue *g_modules;     /* CoDict */

#define EVAL_STACK_SIZE     1000
#define MAX_CALL_DEPTH      30

#define FRAME_EXTERNAL      1

typedef struct {
    int fr_kind;
    CoValue *fr_mod;
    char *fr_pc;
    CoValue **fr_fp;
} Frame;

typedef void (*EFunc_Func)(int num_arg);

EFunc_Func g_efun_table[] = {
    f_printf,
};

static CoValue *start_of_stack[EVAL_STACK_SIZE];
static CoValue **sp;

static Frame frame_stack[MAX_CALL_DEPTH];
static Frame *csp;

#define STACK_INC   (sp++)

#define EXTRACT_UCHAR(p)    (*(unsigned char *)(p))

void Eval_Init(void)
{
    g_modules = CoDict_New();

    sp = start_of_stack - 1;

    csp = frame_stack;
    csp->fr_mod = 0;
    csp->fr_pc = 0;
    csp->fr_fp = 0;
}

void Eval_Destruct(void)
{
    CO_DECREF(g_modules);
}

static void print_push()
{
    CoValue *str;

    str = CoValue_Str(*sp);
    debug(STACK, "                                  --->[%d]: %s", sp - start_of_stack + 1, CoString_AS_STRING(str));
    CO_DECREF(str);
}

static void print_pop()
{
    CoValue *str;

    str = CoValue_Str(*sp);
    debug(STACK, "                                  <---[%d]: %s", sp - start_of_stack + 1, CoString_AS_STRING(str));
    CO_DECREF(str);
}

static void push_number(int num)
{
    STACK_INC;

    *sp = CoInt_FromInt(num);
    print_push();
}

static void push_undefineds(int n)
{
    while (n--) {
        STACK_INC;
        *sp = CoInt_FromInt(0);
        print_push();
    }
}

static void push_lvalue(CoValue **ptr)
{
    STACK_INC;
    *sp = CoLValue_New(ptr);
    print_push();
}

void Eval_Push(CoValue *val)
{
    STACK_INC;
    CO_INCREF(val);
    *sp = val;
    print_push();
}

CoValue *Eval_Top(void)
{
    assert(sp >= start_of_stack);

    return *sp;
}

static CoValue *second(void)
{
    assert(sp - 1 >= start_of_stack);

    return *(sp - 1);
}

void Eval_Pop(void)
{
    assert(sp >= start_of_stack);

    print_pop();
    CO_DECREF(*sp);
    sp--;
}
static void pop_n_elems(int n)
{
    assert(n >= 0);
    assert(sp - n + 1 >= start_of_stack);

    while (n--) {
    print_pop();
        CO_DECREF(*sp);
        sp--;
    }
}


static void assign(CoValue **ptr, CoValue *val)
{
    CoValue *old;

    old = *ptr;
    *ptr = val;

    CO_INCREF(val);
    CO_XDECREF(old);
}

static CoValue **get_local_ptr()
{
    return csp->fr_fp + EXTRACT_UCHAR(csp->fr_pc++);
}

static CoValue **get_global_ptr()
{
    return CoModule_GetGlobalPtrByIndex(csp->fr_mod, EXTRACT_UCHAR(csp->fr_pc++));
}

static void push_frame(void)
{
    if (csp == frame_stack + MAX_CALL_DEPTH - 1) {
        /* TODO: call too deep */
        return;
    }

    csp++;
}

static void pop_frame(void)
{
    /* TODO: check */
    csp--;
}

static void setup_variables(int actual, int num_local, int num_arg)
{
    int tmp;

    tmp = actual - num_arg;
    if (tmp > 0) {
        pop_n_elems(tmp);
        push_undefineds(num_local);
    }
    else {
        push_undefineds(num_local - tmp);
    }

    csp->fr_fp = sp - num_local - num_arg + 1;
}

static void eval_instruction(char *p)
{
    static EFunc_Func *oefun_table = g_efun_table - OPCODE_EFUN_BEGIN;
    int instruction;

    csp->fr_pc = p;
    csp->fr_kind |= FRAME_EXTERNAL;

    for (;;) {
        instruction = EXTRACT_UCHAR(csp->fr_pc++);
        switch (instruction) {
            default:
                {
                    /* never reach */
                    break;
                }
            case F_POP_VALUE:
                {
                    debug(EVAL, "F_POP_VALUE");

                    Eval_Pop();
                    break;
                }
            case F_LOCAL_LVALUE:
                {
                    CoValue **local_ptr;

                    debug(EVAL, "F_LOCAL_LVALUE (%d)", EXTRACT_UCHAR(csp->fr_pc));

                    local_ptr = get_local_ptr();
                    push_lvalue(local_ptr);
                    break;
                }
            case F_GLOBAL_LVALUE:
                {
                    CoValue **global_ptr;

                    debug(EVAL, "F_GLOBAL_LVALUE (%d)", EXTRACT_UCHAR(csp->fr_pc));

                    global_ptr = get_global_ptr();
                    push_lvalue(global_ptr);
                    break;
                }
            case F_NUMBER:
                {
                    int i = 0;

                    LOAD_INT(i, csp->fr_pc);

                    debug(EVAL, "F_NUMBER (%d)", i);

                    push_number(i);
                    break;
                }
            case F_LOCAL:
                {
                    CoValue **local_ptr;

                    debug(EVAL, "F_LOCAL (%d)", EXTRACT_UCHAR(csp->fr_pc));

                    local_ptr = get_local_ptr();
                    /* TODO: check destruct */
                    Eval_Push(*local_ptr);
                    break;
                }
            case F_GLOBAL:
                {
                    CoValue **global_ptr;

                    debug(EVAL, "F_GLOBAL (%d)", EXTRACT_UCHAR(csp->fr_pc));

                    global_ptr = get_global_ptr();
                    Eval_Push(*global_ptr);
                    break;
                }
            case F_ADD:
                {
                    CoValue *a;
                    CoValue *b;
                    CoValue *c;

                    debug(EVAL, "F_ADD");

                    b = Eval_Top();
                    a = second();
                    if (COSTRING_CHECK(a) && COSTRING_CHECK(b)) {
                        c = CoString_Concat(a, b);
                        Eval_Pop();
                        Eval_Pop();
                        Eval_Push(c);
                        CO_DECREF(c);
                    }
                    else {
                        /* TODO: add other type */
                        Eval_Pop();
                    }
                    break;
                }
            case F_ASSIGN:
                {
                    CoValue *lvalue;
                    CoValue *val;

                    debug(EVAL, "F_ASSIGN");

                    lvalue = Eval_Top();
                    val = second();
                    if (COLVALUE_CHECK(lvalue)) {
                        assign(CoLValue_GET_LVALUE(lvalue), val);
                    }
                    else {
                        /* TODO: Bad argument to F_ASSIGN */
                    }

                    Eval_Pop();
                    /* rvalue is already in the correct place */
                    break;
                }
            case F_VOID_ASSIGN:
                {
                    CoValue *lvalue;
                    CoValue *val;

                    debug(EVAL, "F_VOID_ASSIGN");

                    lvalue = Eval_Top();
                    val = second();
                    if (COLVALUE_CHECK(lvalue)) {
                        assign(CoLValue_GET_LVALUE(lvalue), val);
                    }
                    else {
                        /* TODO: Bad argument to F_VOID_ASSIGN */
                    }

                    Eval_Pop();
                    Eval_Pop(); /* rvalue pop too */
                    break;
                }
            case F_VOID_ASSIGN_LOCAL:
                {
                    CoValue **local_ptr;
                    CoValue *val;

                    debug(EVAL, "F_VOID_ASSIGN_LOCAL (%d)", EXTRACT_UCHAR(csp->fr_pc));

                    val = Eval_Top();
                    local_ptr = get_local_ptr();
                    assign(local_ptr, val);
                    Eval_Pop();
                    break;
                }
            case F_CALL_FUNCTION_BY_ADDRESS:
                {
                    int index = 0;
                    int num_arg;
                    CoValue *mod;
                    CoValue *func;

                    LOAD_SHORT(index, csp->fr_pc);

                    debug(EVAL, "F_CALL_FUNCTION_BY_ADDRESS (%d)", index);

                    mod = csp->fr_mod;
                    func = CoModule_GetFunctionByIndex(csp->fr_mod, index);
                    num_arg = EXTRACT_UCHAR(csp->fr_pc++);

                    push_frame();
                    csp->fr_kind = 0;
                    csp->fr_mod = mod;
                    setup_variables(
                            num_arg,
                            CoFunc_GET_NUM_LOCAL(func),
                            CoFunc_GET_NUM_ARG(func));
                    csp->fr_pc = CoFunc_GET_CODE(func);

                    break;
                }
            case F_CONST0:
                {
                    debug(EVAL, "F_CONST0");

                    push_number(0);
                    break;
                }
            case F_STRING:
                {
                    int index = 0;
                    CoValue *str;

                    LOAD_SHORT(index, csp->fr_pc);

                    debug(EVAL, "F_STRING (%d)", index);

                    str = CoModule_GetStringByIndex(csp->fr_mod, index);
                    Eval_Push(str);
                    break;
                }
            case F_RETURN_ZERO:
                {
                    debug(EVAL, "F_RETURN_ZERO (stack = %d)", sp - start_of_stack + 1);

                    pop_n_elems(sp - csp->fr_fp + 1);
                    push_number(0);
                    pop_frame();

                    if (csp[1].fr_kind & FRAME_EXTERNAL) {
                        return;
                    }
                    break;
                }
            case F_RETURN:
                {
                    CoValue *ret;

                    debug(EVAL, "F_RETURN (stack = %d)", sp - start_of_stack + 1);

                    if (sp - csp->fr_fp + 1) {
                        ret = Eval_Top();
                        CO_INCREF(ret);

                        pop_n_elems(sp - csp->fr_fp + 1);

                        Eval_Push(ret);
                        CO_DECREF(ret);
                    }
                    pop_frame();

                    if (csp[1].fr_kind & FRAME_EXTERNAL) {
                        return;
                    }
                    break;
                }
            case F_EFUNV:
                {
                    int num_arg, efun_id;

                    debug(EVAL, "F_EFUNV");

                    num_arg = EXTRACT_UCHAR(csp->fr_pc++);
                    efun_id = EXTRACT_UCHAR(csp->fr_pc++);

                    (*oefun_table[efun_id]) (num_arg);

                    break;
                }
        }
    }
}

static int apply_low(CoValue *mod, char *raw_func, int num_arg)
{
    CoValue *func_name;
    CoValue *func;

    func_name = CoString_FromString(raw_func);
    func = CoModule_GetFunctionByName(mod, func_name);
    CO_DECREF(func_name);
    if (func == NULL) {
        pop_n_elems(num_arg);
        return 0;
    }

    push_frame();
    csp->fr_kind = 0;
    csp->fr_mod = mod;
    setup_variables(
            num_arg,
            CoFunc_GET_NUM_LOCAL(func),
            CoFunc_GET_NUM_ARG(func));

    eval_instruction(CoFunc_GET_CODE(func));

    return 1;
}

CoValue *Eval_FindModule(char *raw_mod)
{
    static char real_name[MAX_MOD_NAME_LEN + 10];
    int fd;
    int length;
    CoValue *mod_name;
    CoValue *mod;

    length = snprintf(real_name, MAX_MOD_NAME_LEN, "%s", raw_mod);
    if (length < 0 || length >= MAX_MOD_NAME_LEN) {
        /* TODO: name too long */
        return NULL;
    }
    real_name[length] = '\0';

    mod_name = CoString_FromString(real_name);
    mod = CoDict_GetItem(g_modules, mod_name);

    if (mod != NULL) {
        CO_DECREF(mod_name);
        return mod;
    }

    real_name[length] = '.';
    real_name[length+1] = 'c';
    real_name[length+2] = '\0';

    fd = open(real_name, O_RDONLY);
    if (fd == -1) {
        printf("Could not read file '%s'.\n", real_name);
        return NULL;
    }

    mod = Compiler_CompileFile(mod_name, fd);

    close(fd);

    if (mod != NULL) {
        CoDict_SetItem(g_modules, mod_name, mod);
    }

    push_frame();
    csp->fr_kind = 0;
    csp->fr_mod = mod;
    setup_variables(0, 0, 0);

    eval_instruction(CoModule_GET_CODE(mod));

    /* discard the return */
    Eval_Pop();

    CO_DECREF(mod_name);
    return mod;
}
